/****************************************************************
 ** Product  :   HP Subscriber Network Application Policy
 ** Module   :   gr-manager
 ** Date: Apr 10, 2015               
 ** Author: Joey Yi
 ** (C) Copyright 2015, Hewlett-Packard Company, All Rights Reserved.
 ** This software is the proprietary information of HP, Inc.  
 ** Use is subject to license terms.
 *****************************************************************
 ** Description:   
 **   Running a command utility class
 **  
 ****************************************************************/
package com.hp.usage.gr.cli;

/**
 * Running a command utility class. 
 *
 * <P>
 * Version: 3.3 <br>
 * Author: Joey Yi
 * Date: Apr 10, 2015
 * 
 * </P>
 *
 **/
import com.hp.siu.logging.Level;
import com.hp.siu.logging.Logger;


import java.io.*;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.regex.Pattern;


/**
 * <p>CmdUtils</p>
 *
 * @author Joey Yi
 */
public class CmdUtils {
    public static final byte[] CRLF = new byte[]{'\n'};

    private static Logger logger = Logger.getLogger("com.hp.usage.gr.snap-gr.CommandInvokeRule", "com.hp.usage.gr.message_catalog");
    private static Pattern[] PASSWORD_FILTER = new Pattern[]{
            Pattern.compile("(-pw\\s+)[^\\s]+"),
            Pattern.compile("(password\\s+)[^\\s]+"),
            Pattern.compile("(-p)[^\\s]+"),
            Pattern.compile("(StartDragent\\.sh\\s+[^\\s]+\\s+[^\\s]+\\s+[^\\s]+\\s+[^\\s]+\\s+[^\\s]+\\s+[^\\s]+\\s+)[^\\s]+"),
            Pattern.compile("(dumper\\.sh\\s+.*-P\\s*)[^\\s]+")
    };

    /**
     * Run a command in a separate process.
     * Throw CommandFailedException if the exit status is non-zero.
     * Command is passed in as a String and is parsed into a
     * String array using a simple method that recognizes <space>
     * and <doublequote> characters only.
     * If the command could include password, {@code couldHavePwd}
     * should be set to true so that this method will try to identify
     * the password and clobber it before printing in the logInfo.
     */
    public static void runCmd(String cmd) throws Exception {
        int exitstatus = runCmdRtnStatus(cmd, null);
        if (exitstatus != 0) {
            throw new Exception("Command failed with exit status not equals 0");
        }
    }



    /**
     * Run a command in a separate process.
     * Return command exit status.
     * Command is passed in as a String and is parsed into a
     * String array using a simple method that recognizes <space>
     * and <doublequote> characters only.
     * If the command could include password, {@code couldHavePwd}
     * should be set to true so that this method will try to identify
     * the password and clobber it before printing in the logInfo.
     */

    public static int runCmdRtnStatus(String cmd, CmdResult cmdResult) throws Exception {
        int exitstatus;
        StreamDrain outStreamDrain;
        StreamDrain errStreamDrain;
        try {
            logger.log(Level.DEBUG, "Executing '" + printableCommand(cmd) + "'");

            // Parse the command string into an array.
            String[] cmdArray = parseCmd(cmd);
            // Execute the command in a subprocess.
            Process proc = Runtime.getRuntime().exec(cmdArray);
            // Launch threads to read the process STDOUT and STDERR.
            outStreamDrain = new StreamDrain(proc.getInputStream(), cmdArray[0], cmdResult == null ? null : cmdResult.getStdout());
            outStreamDrain.start();
            errStreamDrain = new StreamDrain(proc.getErrorStream(), cmdArray[0], cmdResult == null ? null : cmdResult.getStderr());
            errStreamDrain.start();
            // Wait for process termination and get exit status.
            exitstatus = proc.waitFor();
            // Join up with output threads (which should have terminated by now).
            outStreamDrain.join();
            errStreamDrain.join();
        } catch (Exception e) {
            throw new Exception("Unable to run cmd:" + e.getMessage());
        }
        return exitstatus;
    }

    public static String printableCommand(String cmd) {
        String result = cmd;
        for (Pattern pattern : PASSWORD_FILTER) {
            result = pattern.matcher(result).replaceAll("$1******");
        }
        return result;
    }

    public static int runShellScript(String cmd) throws Exception {
        return runShellScript(cmd, null, true);
    }

    public static int runShellScript(String cmd, CmdResult result) throws Exception {
        return runShellScript(cmd, result, true);
    }

    public static int runShellScript(String cmd, CmdResult result, boolean fileMode) throws Exception {
        int exitstatus;
        StreamDrain outStreamDrain;
        StreamDrain errStreamDrain;
        File shfile = null;
        try {
            logger.logDebug("Executing '" + printableCommand(cmd) + "'");
            String[] cmdArray;
            if (fileMode) {
                shfile = outputToFile(cmd);
                cmdArray = new String[]{"/bin/sh", shfile.getAbsolutePath()};
            } else {
                // Parse the command string into an array.
                cmdArray = new String[]{"/bin/sh", "-c", cmd};
            }
            // Execute the command in a subprocess.
            Process proc = Runtime.getRuntime().exec(cmdArray);
            // Launch threads to read the process STDOUT and STDERR.
            outStreamDrain = new StreamDrain(proc.getInputStream(), cmdArray[0], result == null ? null : result.getStdout());
            outStreamDrain.start();
            errStreamDrain = new StreamDrain(proc.getErrorStream(), cmdArray[0], result == null ? null : result.getStderr());
            errStreamDrain.start();
            // Wait for process termination and get exit status.
            exitstatus = proc.waitFor();
            // Join up with output threads (which should have terminated by now).
            outStreamDrain.join();
            errStreamDrain.join();
        } catch (Exception e) {
            throw new Exception("Unable to run cmd:" + e.getMessage());
        } finally {
            if (shfile != null) {
                shfile.delete();
            }
        }
        return exitstatus;
    }

    private static final byte[] SHELL_HEAD = "#!/bin/sh".getBytes();

    public static File outputToFile(String content) throws IOException {
        File result = File.createTempFile("Local", ".sh");
        result.deleteOnExit();

        FileOutputStream fout =null;
        try {
            fout = new FileOutputStream(result);
            fout.write(SHELL_HEAD);
            fout.write(CRLF);
            fout.write(content.getBytes());
            fout.write(CRLF);

            fout.flush();
            fout.close();
        } catch (Exception e) {
        }finally{
            if (fout!=null) {
                fout.close();
            }
        }
        return result;
    }

    /**
     * Parse a command String into a String array using a simple method
     * that recognizes <space> and <doublequote> characters only.
     * Unquoted <space> delimits arguments.
     * <doublequote> quotes strings containing <space> and can be embedded
     * in a substring of a larger argument (e.g. aaa"bb bb"ccc is the
     * same as "aaabb bbcc").
     */
    protected static String[] parseCmd(String cmd) throws Exception {

        // Start with <space> and <doublequote> as delimiters.
        String delims = " \"";
        StringTokenizer cmdTokenizer = new StringTokenizer(cmd, delims, true);

        // Initialize empty vector
        Vector cmdVector = new Vector(10, 10);

        boolean inQuotes = false;
        String arg = null;
        while (cmdTokenizer.hasMoreTokens()) {
            String token = cmdTokenizer.nextToken(delims);
            if (inQuotes) {
                if (token.equals("\"")) {
                    // Add back <space> to delimiters.
                    delims = " \"";
                    inQuotes = false;
                } else {
                    // append token to arg
                    arg = arg + token;
                }
            } else {
                if (token.equals("\"")) {
                    if (arg == null) {
                        // set argument to empty string
                        arg = "";
                    }
                    inQuotes = true;
                    // Remove <space> from delimiters.
                    delims = "\"";
                } else if (token.equals(" ")) {
                    if (arg != null) {
                        // push arg onto vector
                        cmdVector.add(arg);
                    }
                    arg = null;
                } else {
                    // add token to arg (may append)
                    if (arg == null) {
                        arg = token;
                    } else {
                        arg = arg + token;
                    }
                }
            }
        }
        // handle pending arg if any
        if (arg != null) {
            // push arg onto vector
            cmdVector.add(arg);
        }

        // Convert vector to string array and return it.
        String[] cmdArray = new String[cmdVector.size()];
        cmdVector.copyInto(cmdArray);

        return cmdArray;
    }

    static class StreamDrain extends Thread {
        InputStream stream_;
        String progName_;
        StringBuffer sb;

        public StreamDrain(InputStream stream, String progName, StringBuffer sb) {
            stream_ = stream;
            progName_ = progName;
            this.sb = sb;
        }

        public void run() {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream_));
            try {
                String line;
                while ((line = reader.readLine()) != null) {
                    logger.logDebug(line);
                    if (sb != null) sb.append(line).append("\n");
                }
            } catch (IOException e) {
                logger.logError("Error draining process output (" + progName_ + "): " + e.getMessage());
            }
            try {
                reader.close();
            } catch (IOException e) {
                // ignore errors
            }
        }
    }
}
